/***************************************************
 * 此文件的版权归属快程乐码信息’科技有限公司所有，未经允许，请勿复制
 */
package com.kclm.se.io;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * @author yejf
 * @since JDK8.0
 * @CreateDate 2020年8月29日 上午9:53:53 
 * @description 此类用来描述了 在单线程环境下多路复用器[multiplexing]的使用
 *
 * 多路复用器：
 *   1. select 或 poll,  通过“打电话”给内核【kernal】，由内核告诉“我”哪些通道可以读、写，每次都要重复这个过程，内核不会记录
 *   	所以，它的弊端就是：
 *   		A. 每次重复传递数据
 *   		B. 每次调用要触发内核遍历
 *   2. epoll,  让内核创建一个小本本【也就是申请一个空间】用来记录
 *   
 *  下面的代码如果有 linux系统下运行的话，默认使用的就是 epoll
 */
public class SocketMultiplexingSingleThreadv1 {
	/** 基于TPC的服务器的网络通道*/
	private ServerSocketChannel server;
	/** 选对器 Selector对象， 它就是 多路复用器 在java代码中的抽象，它的实现由底层OS决定，可以是 select,poll,epoll, kqueue等 */
	private Selector selector; //linux 多路复用器(select poll epoll kqueue) nginx event{}
	/** 端口 */
	private int port = 9090;
	
	/****
	 * 初始化服务器
	 */
	public void initServer() {
		try {
			//创建ServerSocketChannel
			this.server = ServerSocketChannel.open();
			//绑定端口
			this.server.bind(new InetSocketAddress(port));
			//设置为非阻塞
			this.server.configureBlocking(false);
			
			//创建多路复用器
			this.selector = Selector.open();
			
			//为 ServerSocketChannel 注册选择器
			// servler 约等于 listen状态的 fd4
			/*
			 * register
			 * 如果：
			 * select, poll: jvm里开辟一个数组  fd4 放进去
			 * epoll:  
			 * 		epoll_create()
			 * 		epoll_ctl(fd3, ADD, fd4, EPOLLIN)
			 * 		epoll_wait()
			 */
			//把当前的server 注册到 这个选择器中【多路复用器】, 注册的事件是接收事件
			this.server.register(this.selector, SelectionKey.OP_ACCEPT);
			
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/***
	 * 开启服务
	 */
	public void start() {
		//初始化服务器
		this.initServer();
		System.out.println("服务器启动了, 端口："+this.port);
		try {
			while(true) { //死循环
				// 获取这个选择器中所有注册过的 "路"
				Set<SelectionKey> keys = selector.keys(); //
				System.out.printf("keys的大小：%d\n", keys.size());
				
				//从每一路 来读取数据
				while(selector.select() > 0) { //select() 方法阻塞,直到注册到此选择器的通道中有事件就绪为止
					//返回有状态的fd集合
					Set<SelectionKey> selectionKeys = selector.selectedKeys();
					System.out.println("selectionkeys大小："+selectionKeys.size());
					//迭代
					Iterator<SelectionKey> iter = selectionKeys.iterator();
					//so, 管你啥多路复用器，你呀只能给我状态，我还得一个一个的去处理他们的R/W，同步好辛苦！！！！
					//NIO 自己对着每一个fd调用系统调用，浪费资源，那么你看，这里是不是调用了一次select方法
					//..
					
					while(iter.hasNext()) {
						//第一次只有 server[只有server注册了]
						SelectionKey key = iter.next();
						iter.remove(); // set  不移除会重复循环处理
						//判断
						if(key.isAcceptable()) {
							//看代码的时候，这里是重点，如果要去接爱一个新的连接
							System.out.println("此时是 接收的状态：OP_ACCEPTABLE ");
							acceptHandler(key); 
							
						} else if(key.isReadable()) {
							
							System.out.println("此时是 可读的状态：OP_READ ");
							readHandler(key); //连接 read 还有write 都处理了
							
						}
					}
				}
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	}

	/****
	 * 读数据
	 * @param key
	 */
	private void readHandler(SelectionKey key) {
		try {
			SocketChannel client = (SocketChannel) key.channel();
			client.configureBlocking(false);
			
			//
			ByteBuffer buffer = ByteBuffer.allocate(4096);
			//
			int num = client.read(buffer);
			if(num > 0) {
				//取出来
				buffer.flip();
				//
				byte[] result = new byte[buffer.limit()];
				buffer.get(result);
				//
				String resultStr = new String(result);
				System.out.println("结果：\n"+resultStr);
				//
				buffer.clear();
			} else {
				//System.out.println("没有读到....");
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/****
	 * 处理接收的数据
	 * @param key
	 */
	private void acceptHandler(SelectionKey key) {
		try {
			ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
			SocketChannel client = ssc.accept(); //接受客户端的连接请求
			client.configureBlocking(false); //把客户端连接配置成 非阻塞
			
			//开始读[现 SocketNIO中 的代码一样]
			ByteBuffer buffer = ByteBuffer.allocate(4096);
			//
			//下面很关键， 把这个新客户端 注册进 选择器, 注册的是 读 事件
			client.register(this.selector, SelectionKey.OP_READ, buffer);
			//
			System.out.println("------------------------------------------------");
			System.out.printf("新客户端：%s\n", client.getRemoteAddress());
			System.out.println("------------------------------------------------");
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) {
		SocketMultiplexingSingleThreadv1 v1 = new SocketMultiplexingSingleThreadv1();
		v1.start();
	}
}


